/*
 *
 *    Copyright (c) 2021 Project CHIP Authors
 *    All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#pragma once

#include <app/AttributePathParams.h>
#include <app/InteractionModelEngine.h>
#include <app/ReadPrepareParams.h>
#include <controller/TypedReadCallback.h>

namespace chip {
namespace Controller {
namespace detail {

template <typename DecodableAttributeType>
struct ReportAttributeParams : public app::ReadPrepareParams
{
    ReportAttributeParams(const SessionHandle & sessionHandle) : app::ReadPrepareParams(sessionHandle)
    {
        mKeepSubscriptions = false;
    }
    typename TypedReadAttributeCallback<DecodableAttributeType>::OnSuccessCallbackType mOnReportCb;
    typename TypedReadAttributeCallback<DecodableAttributeType>::OnErrorCallbackType mOnErrorCb;
    typename TypedReadAttributeCallback<DecodableAttributeType>::OnSubscriptionEstablishedCallbackType
        mOnSubscriptionEstablishedCb             = nullptr;
    app::ReadClient::InteractionType mReportType = app::ReadClient::InteractionType::Read;
};

template <typename DecodableAttributeType>
CHIP_ERROR ReportAttribute(Messaging::ExchangeManager * exchangeMgr, EndpointId endpointId, ClusterId clusterId,
                           AttributeId attributeId, ReportAttributeParams<DecodableAttributeType> && readParams)
{
    app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
    CHIP_ERROR err                       = CHIP_NO_ERROR;

    auto readPaths = Platform::MakeUnique<app::AttributePathParams>(endpointId, clusterId, attributeId);
    VerifyOrReturnError(readPaths != nullptr, CHIP_ERROR_NO_MEMORY);
    readParams.mpAttributePathParamsList    = readPaths.get();
    readParams.mAttributePathParamsListSize = 1;

    auto onDone = [](TypedReadAttributeCallback<DecodableAttributeType> * callback) { chip::Platform::Delete(callback); };

    auto callback = chip::Platform::MakeUnique<TypedReadAttributeCallback<DecodableAttributeType>>(
        clusterId, attributeId, readParams.mOnReportCb, readParams.mOnErrorCb, onDone, readParams.mOnSubscriptionEstablishedCb);
    VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);

    auto readClient =
        chip::Platform::MakeUnique<app::ReadClient>(engine, exchangeMgr, callback->GetBufferedCallback(), readParams.mReportType);
    VerifyOrReturnError(readClient != nullptr, CHIP_ERROR_NO_MEMORY);

    if (readClient->IsSubscriptionType())
    {
        readPaths.release();
        err = readClient->SendAutoResubscribeRequest(std::move(readParams));
        ReturnErrorOnFailure(err);
    }
    else
    {
        err = readClient->SendRequest(readParams);
        ReturnErrorOnFailure(err);
    }

    //
    // At this point, we'll get a callback through the OnDone callback above regardless of success or failure
    // of the read operation to permit us to free up the callback object. So, release ownership of the callback
    // object now to prevent it from being reclaimed at the end of this scoped block.
    //
    callback->AdoptReadClient(std::move(readClient));
    callback.release();

    return err;
}

} // namespace detail

/**
 * To avoid instantiating all the complicated read code on a per-attribute
 * basis, we have a helper that's just templated on the type.
 */
template <typename DecodableAttributeType>
CHIP_ERROR ReadAttribute(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
                         ClusterId clusterId, AttributeId attributeId,
                         typename TypedReadAttributeCallback<DecodableAttributeType>::OnSuccessCallbackType onSuccessCb,
                         typename TypedReadAttributeCallback<DecodableAttributeType>::OnErrorCallbackType onErrorCb,
                         bool fabricFiltered = true)
{
    detail::ReportAttributeParams<DecodableAttributeType> params(sessionHandle);
    params.mOnReportCb       = onSuccessCb;
    params.mOnErrorCb        = onErrorCb;
    params.mIsFabricFiltered = fabricFiltered;
    return detail::ReportAttribute(exchangeMgr, endpointId, clusterId, attributeId, std::move(params));
}

/*
 * A typed read attribute function that takes as input a template parameter that encapsulates the type information
 * for a given attribute as well as callbacks for success and failure and either returns a decoded cluster-object representation
 * of the requested attribute through the provided success callback or calls the provided failure callback.
 *
 * The AttributeTypeInfo is generally expected to be a ClusterName::Attributes::AttributeName::TypeInfo struct, but any
 * object that contains type information exposed through a 'DecodableType' type declaration as well as GetClusterId() and
 * GetAttributeId() methods is expected to work.
 */
template <typename AttributeTypeInfo>
CHIP_ERROR
ReadAttribute(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
              typename TypedReadAttributeCallback<typename AttributeTypeInfo::DecodableType>::OnSuccessCallbackType onSuccessCb,
              typename TypedReadAttributeCallback<typename AttributeTypeInfo::DecodableType>::OnErrorCallbackType onErrorCb,
              bool fabricFiltered = true)
{
    return ReadAttribute<typename AttributeTypeInfo::DecodableType>(
        exchangeMgr, sessionHandle, endpointId, AttributeTypeInfo::GetClusterId(), AttributeTypeInfo::GetAttributeId(), onSuccessCb,
        onErrorCb, fabricFiltered);
}

// Helper for SubscribeAttribute to reduce the amount of code generated.
template <typename DecodableAttributeType>
CHIP_ERROR SubscribeAttribute(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
                              ClusterId clusterId, AttributeId attributeId,
                              typename TypedReadAttributeCallback<DecodableAttributeType>::OnSuccessCallbackType onReportCb,
                              typename TypedReadAttributeCallback<DecodableAttributeType>::OnErrorCallbackType onErrorCb,
                              uint16_t minIntervalFloorSeconds, uint16_t maxIntervalCeilingSeconds,
                              typename TypedReadAttributeCallback<DecodableAttributeType>::OnSubscriptionEstablishedCallbackType
                                  onSubscriptionEstablishedCb = nullptr,
                              bool fabricFiltered = true, bool keepPreviousSubscriptions = false)
{
    detail::ReportAttributeParams<DecodableAttributeType> params(sessionHandle);
    params.mOnReportCb                  = onReportCb;
    params.mOnErrorCb                   = onErrorCb;
    params.mOnSubscriptionEstablishedCb = onSubscriptionEstablishedCb;
    params.mMinIntervalFloorSeconds     = minIntervalFloorSeconds;
    params.mMaxIntervalCeilingSeconds   = maxIntervalCeilingSeconds;
    params.mKeepSubscriptions           = keepPreviousSubscriptions;
    params.mReportType                  = app::ReadClient::InteractionType::Subscribe;
    params.mIsFabricFiltered            = fabricFiltered;
    return detail::ReportAttribute(exchangeMgr, endpointId, clusterId, attributeId, std::move(params));
}

/*
 * A typed way to subscribe to the value of a single attribute.  See
 * documentation for ReadAttribute above for details on how AttributeTypeInfo
 * works.
 */
template <typename AttributeTypeInfo>
CHIP_ERROR SubscribeAttribute(
    Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
    typename TypedReadAttributeCallback<typename AttributeTypeInfo::DecodableType>::OnSuccessCallbackType onReportCb,
    typename TypedReadAttributeCallback<typename AttributeTypeInfo::DecodableType>::OnErrorCallbackType onErrorCb,
    uint16_t aMinIntervalFloorSeconds, uint16_t aMaxIntervalCeilingSeconds,
    typename TypedReadAttributeCallback<typename AttributeTypeInfo::DecodableType>::OnSubscriptionEstablishedCallbackType
        onSubscriptionEstablishedCb = nullptr,
    bool fabricFiltered = true, bool keepPreviousSubscriptions = false)
{
    return SubscribeAttribute<typename AttributeTypeInfo::DecodableType>(
        exchangeMgr, sessionHandle, endpointId, AttributeTypeInfo::GetClusterId(), AttributeTypeInfo::GetAttributeId(), onReportCb,
        onErrorCb, aMinIntervalFloorSeconds, aMaxIntervalCeilingSeconds, onSubscriptionEstablishedCb, fabricFiltered,
        keepPreviousSubscriptions);
}

namespace detail {

template <typename DecodableEventType>
struct ReportEventParams : public app::ReadPrepareParams
{
    ReportEventParams(const SessionHandle & sessionHandle) : app::ReadPrepareParams(sessionHandle) { mKeepSubscriptions = false; }
    typename TypedReadEventCallback<DecodableEventType>::OnSuccessCallbackType mOnReportCb;
    typename TypedReadEventCallback<DecodableEventType>::OnErrorCallbackType mOnErrorCb;
    typename TypedReadEventCallback<DecodableEventType>::OnSubscriptionEstablishedCallbackType mOnSubscriptionEstablishedCb =
        nullptr;
    app::ReadClient::InteractionType mReportType = app::ReadClient::InteractionType::Read;
};

template <typename DecodableEventType>
CHIP_ERROR ReportEvent(Messaging::ExchangeManager * apExchangeMgr, EndpointId endpointId,
                       ReportEventParams<DecodableEventType> && readParams)
{
    ClusterId clusterId                  = DecodableEventType::GetClusterId();
    EventId eventId                      = DecodableEventType::GetEventId();
    app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
    CHIP_ERROR err                       = CHIP_NO_ERROR;

    auto readPaths = Platform::MakeUnique<app::EventPathParams>(endpointId, clusterId, eventId);
    VerifyOrReturnError(readPaths != nullptr, CHIP_ERROR_NO_MEMORY);

    readParams.mpEventPathParamsList = readPaths.get();

    readParams.mEventPathParamsListSize = 1;

    auto onDone = [](TypedReadEventCallback<DecodableEventType> * callback) { chip::Platform::Delete(callback); };

    auto callback = chip::Platform::MakeUnique<TypedReadEventCallback<DecodableEventType>>(
        readParams.mOnReportCb, readParams.mOnErrorCb, onDone, readParams.mOnSubscriptionEstablishedCb);

    VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);

    auto readClient = chip::Platform::MakeUnique<app::ReadClient>(engine, apExchangeMgr, *callback.get(), readParams.mReportType);
    VerifyOrReturnError(readClient != nullptr, CHIP_ERROR_NO_MEMORY);

    if (readClient->IsSubscriptionType())
    {
        readPaths.release();
        err = readClient->SendAutoResubscribeRequest(std::move(readParams));
        ReturnErrorOnFailure(err);
    }
    else
    {
        err = readClient->SendRequest(readParams);
        ReturnErrorOnFailure(err);
    }

    //
    // At this point, we'll get a callback through the OnDone callback above regardless of success or failure
    // of the read operation to permit us to free up the callback object. So, release ownership of the callback
    // object now to prevent it from being reclaimed at the end of this scoped block.
    //
    callback->AdoptReadClient(std::move(readClient));
    readClient.release();

    return err;
}

} // namespace detail

/*
 * A typed read event function that takes as input a template parameter that encapsulates the type information
 * for a given attribute as well as callbacks for success and failure and either returns a decoded cluster-object representation
 * of the requested attribute through the provided success callback or calls the provided failure callback.
 *
 * The DecodableEventType is generally expected to be a ClusterName::Events::EventName::DecodableEventType struct, but any
 * object that contains type information exposed through a 'DecodableType' type declaration as well as GetClusterId() and
 * GetEventId() methods is expected to work.
 */
template <typename DecodableEventType>
CHIP_ERROR ReadEvent(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
                     typename TypedReadEventCallback<DecodableEventType>::OnSuccessCallbackType onSuccessCb,
                     typename TypedReadEventCallback<DecodableEventType>::OnErrorCallbackType onErrorCb)
{
    detail::ReportEventParams<DecodableEventType> params(sessionHandle);
    params.mOnReportCb = onSuccessCb;
    params.mOnErrorCb  = onErrorCb;
    return detail::ReportEvent(exchangeMgr, endpointId, std::move(params));
}

/**
 * A functon that allows subscribing to one particular event.  This works
 * similarly to ReadEvent but keeps reporting events as they are emitted.
 */
template <typename DecodableEventType>
CHIP_ERROR SubscribeEvent(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle, EndpointId endpointId,
                          typename TypedReadEventCallback<DecodableEventType>::OnSuccessCallbackType onReportCb,
                          typename TypedReadEventCallback<DecodableEventType>::OnErrorCallbackType onErrorCb,
                          uint16_t minIntervalFloorSeconds, uint16_t maxIntervalCeilingSeconds,
                          typename TypedReadEventCallback<DecodableEventType>::OnSubscriptionEstablishedCallbackType
                              onSubscriptionEstablishedCb = nullptr)
{
    detail::ReportEventParams<DecodableEventType> params(sessionHandle);
    params.mOnReportCb                  = onReportCb;
    params.mOnErrorCb                   = onErrorCb;
    params.mOnSubscriptionEstablishedCb = onSubscriptionEstablishedCb;
    params.mMinIntervalFloorSeconds     = minIntervalFloorSeconds;
    params.mMaxIntervalCeilingSeconds   = maxIntervalCeilingSeconds;
    params.mReportType                  = app::ReadClient::InteractionType::Subscribe;
    return detail::ReportEvent(exchangeMgr, endpointId, std::move(params));
}

} // namespace Controller
} // namespace chip
